package gogsci

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	"golang.org/x/net/context"
	"io/ioutil"
	"math/rand"
	"strings"
	"time"
)

func init() {
	dockerStage := new(DockerStage)
	dockerStage.cli, _ = client.NewEnvClient()
	registeredStage["docker"] = dockerStage
}

type DockerStage struct {
	cli     *client.Client
	timeout time.Duration
}

// Use DOCKER_HOST to set the url to the docker server.
// Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
// Use DOCKER_CERT_PATH to load the tls certificates from.
// Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
func (d *DockerStage) RunStage(stage *Stage, buffer *bytes.Buffer) error {

	str, err := d.PrepareImage(stage.Image, stage.ForcePull)

	if err != nil {
		buffer.WriteString(`prepare image failed` + err.Error())
		return err
	}

	buffer.WriteString(str)

	if err != nil {
		return err
	}

	cfg := ContainerConfig{
		ServiceName: stage.Name,
		Ports:       stage.Ports,
		ImgName:     stage.Image,
		Cmd:         stage.Script,
		Env:         stage.Env,
		Daemon:      stage.Daemon,
		Debug:       stage.Debug,
		Volumes:     stage.Volumes,
	}

	str, err = d.RunContainer(cfg)

	if str != "" {
		fmt.Fprintln(buffer, str)
	}

	if err != nil {
		fmt.Fprintln(buffer, err)
	}

	return err
}

const (
	lib = "docker.io/library/"
)

func imageFullName(pullFullName string) string {
	if !strings.Contains(pullFullName, ":") {
		pullFullName = pullFullName + ":latest"
	}
	if strings.Index(pullFullName, ".") == -1 || strings.Index(pullFullName, ".") > strings.Index(pullFullName, "/") {
		// 不带registry的地址.
		pullFullName = lib + pullFullName
	}

	return pullFullName
}

func (d *DockerStage) PrepareImage(imgName string, ForcePull bool) (string, error) {

	images, err := d.cli.ImageList(context.Background(), types.ImageListOptions{
		All: false,
	})

	if err != nil {
		return "", err
	}

	var found = false

	if !ForcePull {
		for _, image := range images {
			for _, label := range image.RepoTags {
				if label == imgName {
					found = true
					break
				}
			}
		}
	}

	if !found || ForcePull {

		rc, err := d.cli.ImagePull(context.Background(), imageFullName(imgName), types.ImagePullOptions{})
		if err != nil {
			return "", err
		}
		defer rc.Close()

		b, err := ioutil.ReadAll(rc)
		if err != nil {
			return "", err
		}

		return string(b), nil
	}

	return fmt.Sprintf("image already exists: %s", imgName), nil
}

type ContainerConfig struct {
	ServiceName string
	Volumes     []string
	Ports       []string
	ImgName     string
	Cmd         []string
	Env         map[string]string
	Daemon      bool
	Debug       bool
}

func (c ContainerConfig) getTempServiceName() string {
	var str = "abcdefghijklmnopqrstuvwxyz"

	var tmpSubffix = ""
	for i := 0; i < 10; i ++ {
		tmpSubffix += string(str[rand.Intn(len(str))])
	}

	return c.ServiceName + tmpSubffix
}

// 先创建新的容器，
// 再停止老的容器
// 启动新的容器
// 如果启动失败了，则删除新的容器，重新启动老的容器
// 否则删除老的容器
func (d *DockerStage) RunContainer(cfg ContainerConfig) (str string, err error) {

	var buf = bytes.NewBuffer(nil)
	defer func() {
		str = buf.String()
	}()

	ccfg := &container.Config{
		Cmd:          cfg.Cmd,
		ArgsEscaped:  false,
		Image:        cfg.ImgName,
		ExposedPorts: make(nat.PortSet),
	}

	hcfg := &container.HostConfig{}

	var exportPorts map[string]nat.Port // local -> container

	// parse export Ports
	if cfg.Ports != nil {
		exportPorts = make(map[string]nat.Port)
		//ExposedPorts:    nat.PortSet{"8080": struct{}{}}, // export port!
		for _, p := range cfg.Ports {
			var localPort, containerPort string
			if strings.Contains(p, ":") {
				arr := strings.Split(p, ":")
				if len(arr) != 2 {
					return str, fmt.Errorf(`error port configure: %s`, p)
				}
				localPort, containerPort = arr[0], arr[1]
			} else {
				localPort, containerPort = p, p
			}
			exportPorts[localPort] = nat.Port(containerPort)
			ccfg.ExposedPorts[nat.Port(containerPort)] = struct{}{}
		}
	}
	if exportPorts != nil {
		// PortBindings: map[nat.Port][]nat.PortBinding{nat.Port("8080"): {{HostIP: "127.0.0.1", HostPort: "8080"}}},
		hcfg.PortBindings = make(map[nat.Port][]nat.PortBinding)

		for l, c := range exportPorts {
			if _, ok := hcfg.PortBindings[nat.Port(c)]; !ok {
				hcfg.PortBindings[c] = make([]nat.PortBinding, 0)
			}
			var ip, port string
			if strings.Index(l, ":") > 0 {
				arr := strings.Split(l, ":")
				if len(arr) != 2 {
					return str, fmt.Errorf(`error port configure: %s`, l)
				}
				ip, port = arr[0], arr[1]
			}
			hcfg.PortBindings[c] = append(hcfg.PortBindings[c], nat.PortBinding{
				HostIP:   ip,
				HostPort: port,
			})
		}
	}

	// parse Volumes.
	// Type:   mount.TypeBind,
	// Source: "/Users/mrj/Desktop/gogsci/examples",
	// Target: "/data",
	if cfg.Volumes != nil && len(cfg.Volumes) != 0 {
		hcfg.Mounts = make([]mount.Mount, 0)
		for _, v := range cfg.Volumes {
			if !strings.Contains(v, ":") {
				return str, fmt.Errorf(`error volumes configure: %s`, v)
			}
			arr := strings.Split(v, ":")
			if len(arr) != 2 {
				return str, fmt.Errorf(`error volumes configure: %s`, v)
			}

			hcfg.Mounts = append(hcfg.Mounts, mount.Mount{
				Type:   mount.TypeBind,
				Source: arr[0],
				Target: arr[1],
			})
		}
	}

	// parse Environments.
	if cfg.Env != nil && len(cfg.Env) > 0 {
		ccfg.Env = make([]string, 0)
		for k, v := range cfg.Env {
			ccfg.Env = append(ccfg.Env, fmt.Sprintf("%s=%s", k, v))
		}
	}

	// parse scripts.
	if cfg.Cmd != nil {
		ccfg.Cmd = cfg.Cmd
	}

	// find the old Container.
	containers, err := d.cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})

	var findContainer *types.Container
	for _, cont := range containers {
		for _, name := range cont.Names {
			if name[1:] == cfg.ServiceName {
				findContainer = &cont
				break
			}
		}

		if findContainer != nil {
			break
		}
	}

	if findContainer != nil {
		t := d.timeout
		// stop it.
		err := d.cli.ContainerStop(context.Background(), findContainer.ID, &t)

		if err != nil {
			return str, err
		}
	}

	tmpName := cfg.getTempServiceName()

	if cfg.Debug {
		b1, _ := json.MarshalIndent(ccfg, "", "\t")
		buf.WriteString(fmt.Sprintf("container config: %s\n", b1))
		b2, _ := json.MarshalIndent(hcfg, "", "\t")
		buf.WriteString(fmt.Sprintf("host config: %s\n", b2))
	}

	fmt.Fprintln(buf, "create container with name:"+tmpName)
	b, err := d.cli.ContainerCreate(context.Background(), ccfg, hcfg, nil, tmpName)
	if err != nil {
		return str, nil
	}

	fmt.Fprintln(buf, "start container with name:"+tmpName)
	err = d.cli.ContainerStart(context.Background(), b.ID, types.ContainerStartOptions{})

	if err != nil {
		return str, err
	}

	if findContainer != nil {
		fmt.Fprintln(buf, "remove old container with name:"+findContainer.ID, findContainer.Names)
		err = d.cli.ContainerRemove(context.Background(), findContainer.ID, types.ContainerRemoveOptions{})
		if err != nil {
			return str, err
		}
	}


	fmt.Fprintln(buf, "rename container with name: "+tmpName, cfg.ServiceName)
	err = d.cli.ContainerRename(context.Background(), tmpName, cfg.ServiceName)
	if err != nil {
		return str, err
	}

	if cfg.Daemon {
		return str, nil
	}

	okCh, erCh := d.cli.ContainerWait(context.Background(), cfg.ServiceName, container.WaitConditionNotRunning)

	var errMsg = ""
	select {
	case okBody := <-okCh:
		if okBody.StatusCode != 0 {
			errMsg = okBody.Error.Message
		}
	case err := <-erCh:
		return str, err
	}

	rc, err := d.cli.ContainerLogs(context.Background(), cfg.ServiceName, types.ContainerLogsOptions{})
	if err != nil {
		return str, nil
	}

	defer rc.Close()
	bb, err := ioutil.ReadAll(rc)
	if err != nil {
		return str, err
	}
	fmt.Fprintln(buf, "container logs: ", string(bb))

	if errMsg != "" {
		fmt.Fprintln(buf, "container err msg : ", errMsg)
	}

	fmt.Fprintln(buf, "done !")

	return str, err
}
